package com.sikaryofficial.qrcode.manager;

import cn.hutool.core.collection.CollUtil;
import com.alibaba.excel.EasyExcelFactory;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.google.common.collect.Lists;
import com.sikaryofficial.common.core.domain.R;
import com.sikaryofficial.common.core.exception.ServiceException;
import com.sikaryofficial.common.core.utils.SpringUtils;
import com.sikaryofficial.common.redis.lock.RedisDistributedLocker;
import com.sikaryofficial.common.redis.service.RedisService;
import com.sikaryofficial.common.security.utils.SecurityUtils;
import com.sikaryofficial.qrcode.config.ProductQrUrlConfig;
import com.sikaryofficial.qrcode.constant.DownloadTaskStatusEnum;
import com.sikaryofficial.qrcode.constant.DownloadTaskTypeEnum;
import com.sikaryofficial.qrcode.domain.dto.resp.QrCodeCountResp;
import com.sikaryofficial.qrcode.domain.entity.BrandTaste;
import com.sikaryofficial.qrcode.domain.entity.DownloadTask;
import com.sikaryofficial.qrcode.domain.mapping.BrandTasteMapping;
import com.sikaryofficial.qrcode.domain.mongo.ProductQrMongo;
import com.sikaryofficial.qrcode.domain.vo.ProductQrExportVO;
import com.sikaryofficial.qrcode.service.IBrandTasteService;
import com.sikaryofficial.qrcode.service.IDownloadTaskService;
import com.sikaryofficial.qrcode.service.ProductQrMongoService;
import com.sikaryofficial.qrcode.service.QrUserDetailMongoService;
import com.sikaryofficial.qrcode.service.brand.cache.BrandTasteCacheService;
import com.sikaryofficial.qrcode.util.ProductQrUtil;
import com.sikaryofficial.system.api.RemoteFileService;
import com.sikaryofficial.system.api.domain.SysFile;
import com.sikaryofficial.system.api.model.product.BrandTasteDTO;
import com.sikaryofficial.system.api.model.qrcode.ProductQRReq;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.text.MessageFormat;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

import static com.sikaryofficial.common.core.constant.CacheBusinessConstants.PRODUCT_QRCODE_LOCK_CACHE;
import static com.sikaryofficial.common.core.constant.CacheBusinessConstants.PRODUCT_QRCODE_STATUS_CACHE;
import static com.sikaryofficial.common.core.utils.file.FileUtils.fileToMultipartFile;

/**
 * @author : qinjinyuan
 * @desc : 产品管理
 * @date : 2023/10/30 15:39
 */
@Service
@Slf4j
@RefreshScope
@SuppressWarnings(value = "unchecked")
public class ProductManager {
    @Resource(name = "ioTaskExecutor")
    private ThreadPoolTaskExecutor ioTaskExecutor;
    @Resource(name = "asyncTaskExecutor")
    private ThreadPoolTaskExecutor asyncTaskExecutor;
    @Autowired
    private RedisService redisService;
    @Autowired
    private RedisDistributedLocker locker;
    @Autowired
    private IBrandTasteService brandTasteService;
    @Autowired
    private IDownloadTaskService downloadTaskService;
    @Autowired
    private ProductQrUrlConfig config;
    @Autowired
    private BrandTasteCacheService brandTasteCache;
    @Autowired
    private ProductQrMongoService productQrService;
    @Autowired
    private QrUserDetailMongoService qrUserDetailMongoService;

    /**
     * 二维码统计
     *
     * @return
     */
    public QrCodeCountResp countQrCodeData() {
        long startTime = System.currentTimeMillis();
        log.info("countQrCodeData method started.");
        // 获取 product_qr_t 集合的总文档数
        long totalProductQr = productQrService.countDocument();
        log.info("countQrCodeData method finished. Time taken: {} ms", (System.currentTimeMillis() - startTime));
        // 获取 qr_user_detail_t 集合的总文档数
        long totalUserDetailQr = qrUserDetailMongoService.countDocument();
        log.info("countQrCodeData method finished. Time taken: {} ms", (System.currentTimeMillis() - startTime));
        // 封装并返回结果
        return QrCodeCountResp.builder()
                .totalProductQr(totalProductQr)
                .totalUserDetailQr(totalUserDetailQr)
                .build();
    }

    /**
     * 二维码生成服务入口
     *
     * @param req
     */
    public void productQRGen(ProductQRReq req) {
        BrandTaste brandTaste = brandTasteService.getById(req.getBrandTasteId());
        if (Objects.isNull(brandTaste)) {
            throw new ServiceException("the product is not exist");
        }
        // 设置锁key
        String lockKey = MessageFormat.format(PRODUCT_QRCODE_LOCK_CACHE, brandTaste.getMd5());
        if (locker.tryLock(lockKey, TimeUnit.MINUTES, 2, 2)) {
            log.info("get the lock.");
            Long userId = SecurityUtils.getUserId();
            String qrcodeStatusKey = MessageFormat.format(PRODUCT_QRCODE_STATUS_CACHE, brandTaste.getMd5());
            redisService.setCacheObject(qrcodeStatusKey, DownloadTaskStatusEnum.GENERATING.getCode(), 1L, TimeUnit.MINUTES);
            try {
                // 初始化任务
                Long taskId = IdWorker.getId();
                initDownloadTask(req, userId, brandTaste, taskId);
                // 文件夹路径
                String taskFolderPath = MessageFormat.format(config.getFolderPath(), taskId.toString());
                File folder = new File(taskFolderPath);
                folder.mkdirs();
                // 生成孩子文件
                genChildFile(req, brandTaste, folder);
                // 更新任务状态
                CompletableFuture.runAsync(() -> {
                    // 输出打包zip文件到服务器本地目录
                    String genLocalZipFile = ProductQrUtil.genLocalZipFile(folder, req.getBrandTasteId(), taskId);
                    RemoteFileService remoteFileService = SpringUtils.getBean(RemoteFileService.class);
                    R<SysFile> sysFileR = remoteFileService.upload(fileToMultipartFile(new File(genLocalZipFile)));
                    String downloadTaskFileUrl = "";
                    if (Objects.nonNull(sysFileR) && Objects.nonNull(sysFileR.getData())) {
                        downloadTaskFileUrl = sysFileR.getData().getUrl();
                    }
                    // 更新task状态
                    updateDownloadTask(userId, taskId, downloadTaskFileUrl, qrcodeStatusKey);
                }, asyncTaskExecutor);
                // 更新缓存数据字段
                CompletableFuture.runAsync(() -> {
                    BrandTasteDTO brandTasteDTO = brandTasteCache.getSignBrandTaste(req.getBrandTasteId());
                    brandTasteDTO.setQrExportNum(brandTasteService.updateQrMaxNum(brandTaste.getMd5(), userId, req.getQrNum()));
                    brandTasteCache.updateBrandTasteCacheOnly(brandTasteDTO);
                }, asyncTaskExecutor);
            } catch (Exception e) {
                log.error("productQRDownload error", e);
                throw new ServiceException("The QrCode zip file export error." + e.getMessage());
            } finally {
                log.info("unlock the lock.");
                locker.unlock(lockKey);
            }
        } else {
            log.info("not get the lock.");
            throw new ServiceException("The QrCode zip file is being generated,please wait.");
        }
    }

    private void genChildFile(ProductQRReq req, BrandTaste brandTaste, File folder) {
        long startTime = System.currentTimeMillis();
        log.info("genChildFile----start-->");
        // 异步任务，生成Excel
        long total = req.getQrNum() % config.getProductQRBatchSize() == 0 ? req.getQrNum() / config.getProductQRBatchSize() : req.getQrNum() / config.getProductQRBatchSize() + 1;
        List<Integer> batchArr = Lists.newArrayList();
        for (int i = 1; i <= total; i++) {
            batchArr.add(i);
        }
        Long qrExportNum = Objects.isNull(brandTaste.getQrExportNum()) ? 0 : brandTaste.getQrExportNum();
        CompletableFuture[] completableFutures = batchArr.stream().map(batchNum -> CompletableFuture.runAsync(() -> genExcelWithBatch(req, batchNum, qrExportNum, folder), ioTaskExecutor)).toArray(CompletableFuture[]::new);
        // 等待所有任务执行完
        CompletableFuture.allOf(completableFutures).join();
        log.info("genChildFile----end--->" + (System.currentTimeMillis() - startTime));
    }

    private void initDownloadTask(ProductQRReq req, Long userId, BrandTaste brandTaste, Long taskId) {
        downloadTaskService.save(DownloadTask.builder().createdTime(new Date()).createdBy(userId).taskId(taskId).taskStatus(DownloadTaskStatusEnum.GENERATING.getCode()).taskType(DownloadTaskTypeEnum.PRODUCT_QRCODE.getCode()).workLoad(req.getQrNum()).objectId(brandTaste.getBrandTasteId()).build());
    }

    private void updateDownloadTask(Long userId, Long taskId, String offlineUrl, String qrcodeStatusKey) {
        downloadTaskService.update(DownloadTask.builder().taskStatus(DownloadTaskStatusEnum.DOWNLOADABLE.getCode()).updatedBy(userId).updatedTime(new Date()).offlineUrl(offlineUrl).build(), new LambdaUpdateWrapper<DownloadTask>().eq(DownloadTask::getTaskId, taskId).eq(DownloadTask::getTaskStatus, DownloadTaskStatusEnum.GENERATING.getCode()));
        // 任务更新后，更新缓存数据字段
        redisService.deleteObject(qrcodeStatusKey);
    }

    private List<ProductQrExportVO> genExcelWithBatch(ProductQRReq req, Integer batchNum, Long qrExportNum, File folder) {
        // 处理待导出数据
        List<ProductQrExportVO> productList = productQRExport(req, batchNum, qrExportNum);
        // 商品二维码数据保存
        saveProductQrCode(productList);
        if ("txt".equalsIgnoreCase(config.getProductQRStoreFileType())) {
            writeDataToTxtFile(productList, batchNum, folder);
        } else if ("xlsx".equalsIgnoreCase(config.getProductQRStoreFileType())) {
            writeDataToExcelFile(productList, batchNum, folder);
        }
        return productList;
    }

    private void saveProductQrCode(List<ProductQrExportVO> productList) {
        long startTime = System.currentTimeMillis();
        log.info("save-start-->");
        // 批量保存数据
        List<ProductQrMongo> productQrs = Lists.newArrayList();
        productList.forEach(vo -> {
            ProductQrMongo productQr = ProductQrMongo.builder()
                    .param(vo.getParam())
                    .qrYearMonth(vo.getQrYearMonth())
                    .productShortId(vo.getProductShortId())
                    .seqNo(vo.getSeqNo())
                    .brandTasteId(vo.getBrandTasteId())
                    .createdBy(0L)
                    .createdTime(new Date())
                    .deletedVersion(0L)
                    .scanCodeNum(0)
                    .hasScanCode(0)
                    .build();
            productQrs.add(productQr);
        });
        // 并行将二维码数据保存到MongoDB
        // 计算批次 ，mongoDB 默认是 1000一个批次执行
        List<List<ProductQrMongo>> partitionData = Lists.partition(productQrs, config.getQrPartitionBatchSize());
        CompletableFuture[] completableFutures = partitionData.stream().map(productQrBatchList -> CompletableFuture.runAsync(() -> productQrService.insertBatch(productQrBatchList), ioTaskExecutor)).toArray(CompletableFuture[]::new);
        // 等待所有任务执行完
        CompletableFuture.allOf(completableFutures).join();
        log.info("save-end-productQrsSize:{},cost:{}ms", productQrs.size(), System.currentTimeMillis() - startTime);
    }

    /**
     * 导出产品二维码
     *
     * @param req
     * @param batchNum
     * @param beforeOffset 之前的起止位置
     * @return
     */
    private List<ProductQrExportVO> productQRExport(ProductQRReq req, Integer batchNum, Long beforeOffset) {
        BrandTasteDTO brandTasteDTO = brandTasteCache.getSignBrandTaste(req.getBrandTasteId());
        if (Objects.isNull(brandTasteDTO)) {
            throw new ServiceException("the product is not exist");
        }
        ProductQrExportVO productQrExportVO = BrandTasteMapping.INSTANCE.coverToExportQrVO(brandTasteDTO);
        // 获取对应的二维码地址模版
        String qrUrlPattern = config.getQrUrlByCode(brandTasteDTO.getBrandNo()).getQrUrl();
        // 构造导出数据
        long start = (long) (batchNum - 1) * config.getProductQRBatchSize() + beforeOffset + 1;
        AtomicLong rowNum = new AtomicLong(start);
        List<ProductQrExportVO> productQrExportVOS = Lists.newArrayList();
        long end = buildEndOffset(req, batchNum, beforeOffset);
        // yyyy *10000 + month * 100 + day
        Integer yearMonth = LocalDateTime.now().getYear() * 10000 + LocalDateTime.now().getMonthValue() * 100
                + LocalDateTime.now().getDayOfMonth();
        String yearMonthStr = ProductQrUtil.encodeDateToStr(LocalDateTime.now().getYear(), LocalDateTime.now().getMonthValue(), LocalDateTime.now().getDayOfMonth());
        for (long i = start; i <= end; i++) {
            ProductQrExportVO target = BrandTasteMapping.INSTANCE.coverQrVOToQrVO(productQrExportVO);
            productQrExportVOS.add(target);
            target.setSeqNo(rowNum.get());
            String param = ProductQrUtil.buildProductScanParam(target.getProductShortId(), target.getSeqNo(), config.getProductQRRandomLength(), yearMonthStr);
            target.setParam(param);
            target.setQrYearMonth(yearMonth);
            target.setQrUrl(MessageFormat.format(qrUrlPattern, param));
            rowNum.getAndIncrement();
        }
        return productQrExportVOS;
    }

    /**
     * 构建结束位置
     *
     * @param req
     * @param batchNum
     * @param beforeOffset
     * @return
     */
    private long buildEndOffset(ProductQRReq req, Integer batchNum, Long beforeOffset) {
        long end = req.getQrNum() > (long) batchNum * config.getProductQRBatchSize() ? (long) batchNum * config.getProductQRBatchSize() : req.getQrNum();
        end = end + beforeOffset;
        return end;
    }

    /**
     * 写入数据到Excel
     *
     * @param productList
     */
    private void writeDataToExcelFile(List<ProductQrExportVO> productList, Integer batchNum, File folder) {
        String tasteName = dealTasteName(productList);
        String fileName = tasteName + batchNum + "_" + IdWorker.getIdStr() + "." + config.getProductQRStoreFileType();
        String filePath = folder.getPath() + File.separator + fileName;
        try {
            EasyExcelFactory.write(Files.newOutputStream(new File(filePath).toPath()), ProductQrExportVO.class).sheet("ske_product").doWrite(productList);
        } catch (IOException e) {
            log.error("downloadQrCode error", e);
        }
    }

    /**
     * 写入数据到txt文件
     *
     * @param productList
     */
    private void writeDataToTxtFile(List<ProductQrExportVO> productList, Integer batchNum, File folder) {
        // 写入数据到Excel
        // TODO: 添加口味名称到最前面
        String tasteName = dealTasteName(productList);
        String fileName = tasteName + batchNum + "_" + IdWorker.getIdStr() + "." + config.getProductQRStoreFileType();
        String filePath = folder.getPath() + File.separator + fileName;
        try (BufferedWriter writer = new BufferedWriter(new FileWriter(filePath))) {
            for (ProductQrExportVO product : productList) {
                writer.write(product.getSeqNo() + "\t" + product.getQrUrl() + "\n");
            }
        } catch (IOException e) {
            log.error("writeDataToTxtFile error", e);
        }
    }

    /**
     * 文件名特殊处理
     *
     * @param productList
     * @return
     */
    private String dealTasteName(List<ProductQrExportVO> productList) {
        if (CollUtil.isEmpty(productList)) {
            return "";
        }
        ProductQrExportVO vo = productList.iterator().next();
        String productName = vo.getProductName();
        String model = vo.getModel();
        String tasteName = vo.getTasteName();
        StringBuilder appendName = new StringBuilder();
        appendName.append(productName).append("_").append(model).append("_").append(tasteName);
        String result = appendName.toString();
        if (StringUtils.isNotBlank(result)) {
            // 口味值特殊处理，将tasteName中的非法文件名字符（包括空格、制表符、换行符以及 / : * ? " < > |）全部替换为下划线
            String invalidCharsRegex = "[\\\\/:*?\"<>| \\t\\n]";
            // 使用正则表达式的replaceAll方法，将匹配到的每个非法字符替换为下划线
            result = result.replaceAll(invalidCharsRegex, "_");
        }
        return result + "_";
    }
}
